Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 034a0a6edefdd9ab64a338e45711f607e35d1c71


Parents : 250ea36
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-04-17T23:28:53-05:00

feat(scripts): add Argos Translate script for JSON localization and new APK signing script

Changes

3 files changed, 444 insertions(+), 6 deletions(-)


Diff

diff --git a/scripts/argos_translate.py b/scripts/argos_translate.py
new file mode 100755
index 00000000..40e96022
--- /dev/null
+++ b/scripts/argos_translate.py
@@ -0,0 +1,266 @@
+#!/usr/bin/env python3
+"""Argos Translate JSON localization script.
+
+This script provides an automated workflow to translate JSON localization files
+(such as `en.json`) to target languages using Argos Translate. It ensures that
+interpolated variables (e.g., `{count}`, `{status}`) are preserved and not
+altered during the translation process.
+
+Requirements:
+ - argostranslate (pip install argostranslate)
+
+Usage:
+ python scripts/argos_translate.py --from en --to zh --input locales/en.json --output locales/zh.json
+
+ You can also use environment variables instead of CLI flags:
+ ARGOS_FROM_LANG="en"
+ ARGOS_TO_LANG="zh"
+ ARGOS_INPUT_FILE="locales/en.json"
+ ARGOS_OUTPUT_FILE="locales/zh.json"
+"""
+
+import argparse
+import json
+import os
+import re
+import sys
+import time
+
+
+# ANSI color codes for terminal output
+class Colors:
+ HEADER = "\033[95m"
+ OKBLUE = "\033[94m"
+ OKCYAN = "\033[96m"
+ OKGREEN = "\033[92m"
+ WARNING = "\033[93m"
+ FAIL = "\033[91m"
+ ENDC = "\033[0m"
+ BOLD = "\033[1m"
+ UNDERLINE = "\033[4m"
+
+
+def print_info(msg):
+ print(f"{Colors.OKCYAN}[INFO]{Colors.ENDC} {msg}")
+
+
+def print_success(msg):
+ print(f"{Colors.OKGREEN}[SUCCESS]{Colors.ENDC} {msg}")
+
+
+def print_warning(msg):
+ print(f"{Colors.WARNING}[WARNING]{Colors.ENDC} {msg}")
+
+
+def print_error(msg):
+ print(f"{Colors.FAIL}[ERROR]{Colors.ENDC} {msg}")
+
+
+try:
+ import argostranslate.package
+ import argostranslate.translate
+except ImportError:
+ print_error("The 'argostranslate' module is not installed.")
+ print_info("Please install it using: pip install argostranslate")
+ sys.exit(1)
+
+
+def ensure_package_installed(from_code, to_code):
+ """Ensure the translation package from `from_code` to `to_code` is installed.
+
+ If not installed, attempts to download and install it automatically.
+ """
+ installed = argostranslate.translate.get_installed_languages()
+ installed_dict = {lang.code: lang for lang in installed}
+
+ from_lang = installed_dict.get(from_code)
+ to_lang = installed_dict.get(to_code)
+
+ if not from_lang or not to_lang or to_lang not in from_lang.translations_from:
+ print_warning(
+ f"Translation package {from_code} -> {to_code} not found. Attempting to install..."
+ )
+ try:
+ argostranslate.package.update_package_index()
+ available_packages = argostranslate.package.get_available_packages()
+
+ pkg_to_install = None
+ for pkg in available_packages:
+ if pkg.from_code == from_code and pkg.to_code == to_code:
+ pkg_to_install = pkg
+ break
+
+ if pkg_to_install:
+ print_info(f"Downloading package: {pkg_to_install}")
+ argostranslate.package.install_from_path(pkg_to_install.download())
+ print_success(
+ f"Successfully installed package: {from_code} -> {to_code}"
+ )
+
+ # Refresh installed languages
+ installed = argostranslate.translate.get_installed_languages()
+ installed_dict = {lang.code: lang for lang in installed}
+ else:
+ print_error(
+ f"Could not find a translation package for {from_code} -> {to_code}"
+ )
+ sys.exit(1)
+ except Exception as e:
+ print_error(f"Failed to install language package: {e}")
+ sys.exit(1)
+
+ return installed_dict.get(from_code), installed_dict.get(to_code)
+
+
+def get_translation_func(from_code, to_code):
+ """Returns a translation function for the specified language pair."""
+ from_lang, to_lang = ensure_package_installed(from_code, to_code)
+ translator = from_lang.get_translation(to_lang)
+ if not translator:
+ print_error(f"No translation available from {from_code} to {to_code}")
+ sys.exit(1)
+ return translator.translate
+
+
+def replace_vars_with_tokens(text):
+ """Replaces `{variable}` patterns with a standard token like `XVAR0X`.
+
+ So the translation engine doesn't attempt to translate variable names.
+ Returns the modified text and the list of found variables.
+ """
+ vars_found = []
+
+ def replacer(match):
+ vars_found.append(match.group(0))
+ return f" XVAR{len(vars_found) - 1}X "
+
+ replaced_text = re.sub(r"\{+.*?\}+", replacer, text)
+ return replaced_text, vars_found
+
+
+def restore_vars_from_tokens(text, vars_found):
+ """Restores the original `{variable}` patterns back into the translated text.
+
+ Looks for the `XVAR0X` tokens.
+ """
+ for i, var in enumerate(vars_found):
+ # The translation engine might change case or spacing around the token
+ text = re.sub(rf"\s*[xX][vV][aA][rR]{i}[xX]\s*", var, text)
+ return text.strip()
+
+
+def translate_dict(data, translate_func, target_name=None):
+ """Recursively iterates over a dictionary and translates all string values.
+
+ Skips the `_languageName` key, which can be explicitly set.
+ """
+ if isinstance(data, dict):
+ new_dict = {}
+ for k, v in data.items():
+ if k == "_languageName" and target_name:
+ new_dict[k] = target_name
+ continue
+ elif k == "_languageName":
+ # Keep original if no target name provided
+ new_dict[k] = v
+ continue
+
+ new_dict[k] = translate_dict(v, translate_func, target_name)
+ return new_dict
+ elif isinstance(data, list):
+ return [translate_dict(item, translate_func, target_name) for item in data]
+ elif isinstance(data, str):
+ if not data.strip():
+ return data
+
+ temp_text, vars_found = replace_vars_with_tokens(data)
+ try:
+ translated_temp = translate_func(temp_text)
+ return restore_vars_from_tokens(translated_temp, vars_found)
+ except Exception as e:
+ print_warning(
+ f"Failed to translate '{data}': {e}. Falling back to original."
+ )
+ return data
+ else:
+ return data
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Translate JSON localization files using Argos Translate."
+ )
+ parser.add_argument(
+ "--from", dest="from_lang", help="Source language code (e.g. 'en')"
+ )
+ parser.add_argument("--to", dest="to_lang", help="Target language code (e.g. 'zh')")
+ parser.add_argument("--input", dest="input_file", help="Path to input JSON file")
+ parser.add_argument("--output", dest="output_file", help="Path to output JSON file")
+ parser.add_argument(
+ "--name",
+ dest="target_name",
+ help="Native name of the target language (e.g. '中文' for Chinese)",
+ )
+
+ args = parser.parse_args()
+
+ # Fallback to Environment Variables if arguments aren't provided
+ from_lang = args.from_lang or os.environ.get("ARGOS_FROM_LANG")
+ to_lang = args.to_lang or os.environ.get("ARGOS_TO_LANG")
+ input_file = args.input_file or os.environ.get("ARGOS_INPUT_FILE")
+ output_file = args.output_file or os.environ.get("ARGOS_OUTPUT_FILE")
+ target_name = args.target_name or os.environ.get("ARGOS_TARGET_NAME")
+
+ if not all([from_lang, to_lang, input_file, output_file]):
+ parser.print_help()
+ print_error(
+ "Missing required arguments. Please provide --from, --to, --input, and --output."
+ )
+ sys.exit(1)
+
+ if not os.path.exists(input_file):
+ print_error(f"Input file not found: {input_file}")
+ sys.exit(1)
+
+ print_info(f"Source Language: {Colors.BOLD}{from_lang}{Colors.ENDC}")
+ print_info(f"Target Language: {Colors.BOLD}{to_lang}{Colors.ENDC}")
+ print_info(f"Input File: {Colors.BOLD}{input_file}{Colors.ENDC}")
+ print_info(f"Output File: {Colors.BOLD}{output_file}{Colors.ENDC}")
+
+ # Load JSON
+ try:
+ with open(input_file, "r", encoding="utf-8") as f:
+ source_data = json.load(f)
+ except json.JSONDecodeError as e:
+ print_error(f"Invalid JSON in input file: {e}")
+ sys.exit(1)
+
+ start_time = time.time()
+
+ # Get Translator
+ translate_func = get_translation_func(from_lang, to_lang)
+
+ print_info(
+ "Starting translation. This may take a moment depending on the file size..."
+ )
+ translated_data = translate_dict(source_data, translate_func, target_name)
+
+ # Ensure output directory exists
+ os.makedirs(os.path.dirname(os.path.abspath(output_file)), exist_ok=True)
+
+ # Save translated JSON
+ try:
+ with open(output_file, "w", encoding="utf-8") as f:
+ json.dump(translated_data, f, ensure_ascii=False, indent=4)
+ f.write("\n")
+ except IOError as e:
+ print_error(f"Could not write to output file: {e}")
+ sys.exit(1)
+
+ elapsed_time = time.time() - start_time
+ print_success(f"Translation complete in {elapsed_time:.2f} seconds!")
+ print_success(f"Output saved to {Colors.BOLD}{output_file}{Colors.ENDC}")
+
+
+if __name__ == "__main__":
+ main()

diff --git a/scripts/build-android-wheels-local.sh b/scripts/build-android-wheels-local.sh
index 31aa70bb..0500521e 100755
--- a/scripts/build-android-wheels-local.sh
+++ b/scripts/build-android-wheels-local.sh
@@ -19,7 +19,7 @@ Options:
--python-minor X.Y Python minor for target wheels (default: 3.11)
--target-version V Explicit Chaquopy target version (default: auto latest for python minor)
--chaquopy-ref REF Chaquopy git ref/commit to checkout (default: master)
- --abis LIST Comma-separated ABIs (default: arm64-v8a,x86_64)
+ --abis LIST Comma-separated ABIs (default: arm64-v8a,x86_64,armeabi-v7a)
--api-level N Android API level for wheel tag (default: 24)
--pycodec2-version V pycodec2 version to build (default: 4.1.1)
--numpy-version V NumPy version used during pycodec2 build (default: 1.26.2)
@@ -36,7 +36,7 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PYTHON_MINOR="3.11"
TARGET_VERSION=""
CHAQUOPY_REF="${CHAQUOPY_REF:-master}"
-ABI_LIST="arm64-v8a,x86_64"
+ABI_LIST="arm64-v8a,x86_64,armeabi-v7a"
API_LEVEL="24"
PYCODEC2_VERSION="4.1.1"
LIBCODEC2_VERSION="1.2.0"
@@ -145,10 +145,26 @@ require_cmd sed
require_cmd awk
require_cmd sort
-PYTHON_BIN="python${PYTHON_MINOR}"
+case ",${ABI_LIST}," in
+ *,armeabi-v7a,*)
+ if [[ -z "${ANDROID_HOME:-}" && -z "${ANDROID_SDK_ROOT:-}" ]]; then
+ echo "armeabi-v7a: NumPy is built from source and needs the Android SDK/NDK (Chaquopy android-env.sh)." >&2
+ echo "Set ANDROID_HOME or ANDROID_SDK_ROOT to your SDK root (with cmdline-tools/latest/bin/sdkmanager)." >&2
+ exit 1
+ fi
+ export ANDROID_HOME="${ANDROID_HOME:-${ANDROID_SDK_ROOT}}"
+ export ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-${ANDROID_HOME}}"
+ if [[ ! -x "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" ]]; then
+ echo "armeabi-v7a: expected sdkmanager at ${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" >&2
+ exit 1
+ fi
+ ;;
+esac
+
+PYTHON_BIN="${PYTHON_BIN:-python${PYTHON_MINOR}}"
if ! command -v "${PYTHON_BIN}" >/dev/null 2>&1; then
- echo "Required interpreter not found on PATH: ${PYTHON_BIN}" >&2
- echo "Install Python ${PYTHON_MINOR} locally before running this script." >&2
+ echo "Required interpreter not found: ${PYTHON_BIN}" >&2
+ echo "Install Python ${PYTHON_MINOR} (e.g. uv python install ${PYTHON_MINOR}) or set PYTHON_BIN." >&2
exit 1
fi
@@ -198,6 +214,28 @@ fi
NUMPY_DIST_DIR="${PYPIDIR}/dist/numpy"
mkdir -p "${NUMPY_DIST_DIR}"
PYTHON_ABI_TAG="cp${PYTHON_MINOR/./}"
+
+# Chaquopy's numpy recipe lists chaquopy-openblas as a host requirement. build-wheel.py
+# only loads it from ${PYPIDIR}/dist/chaquopy-openblas/ (it does not fetch from the index).
+# Official wheels: https://chaquo.com/pypi-13.1/chaquopy-openblas/
+cache_chaquopy_openblas_for_abi() {
+ local abi="$1"
+ local name url
+ mkdir -p "${PYPIDIR}/dist/chaquopy-openblas"
+ case "${abi}" in
+ armeabi-v7a) name="chaquopy_openblas-0.2.20-5-py3-none-android_16_armeabi_v7a.whl" ;;
+ arm64-v8a) name="chaquopy_openblas-0.2.20-5-py3-none-android_21_arm64_v8a.whl" ;;
+ x86_64) name="chaquopy_openblas-0.2.20-5-py3-none-android_21_x86_64.whl" ;;
+ *) return 0 ;;
+ esac
+ url="https://chaquo.com/pypi-13.1/chaquopy-openblas/${name}"
+ if [[ -f "${PYPIDIR}/dist/chaquopy-openblas/${name}" ]]; then
+ return 0
+ fi
+ echo "Caching Chaquopy OpenBLAS wheel for ${abi}"
+ curl -fsSL -o "${PYPIDIR}/dist/chaquopy-openblas/${name}" "${url}"
+}
+
for abi in ${ABI_LIST//,/ }; do
platform_tag="$(abi_to_platform_tag "${abi}")"
echo "Resolving NumPy wheel for ABI ${abi} (${platform_tag})"
@@ -213,12 +251,12 @@ for abi in ${ABI_LIST//,/ }; do
--extra-index-url https://chaquo.com/pypi-13.1 \
--dest "${NUMPY_DIST_DIR}"; then
echo "No prebuilt NumPy wheel for ${abi}; building locally via Chaquopy recipe"
+ cache_chaquopy_openblas_for_abi "${abi}"
"${VENV_DIR}/bin/python" "${PYPIDIR}/build-wheel.py" \
--python "${PYTHON_MINOR}" \
--api-level "${API_LEVEL}" \
--abi "${abi}" \
"${PYPIDIR}/packages/numpy"
- cp -f "${PYPIDIR}/dist/numpy"/numpy-"${NUMPY_VERSION}"-*.whl "${NUMPY_DIST_DIR}/"
fi
done

diff --git a/scripts/sign-android-apks.sh b/scripts/sign-android-apks.sh
new file mode 100755
index 00000000..3492b3db
--- /dev/null
+++ b/scripts/sign-android-apks.sh
@@ -0,0 +1,134 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+usage() {
+ cat <<'EOF'
+Sign Android release APKs (and optionally add SourceStamp).
+
+Required env vars:
+ SIGNING_KEYSTORE_PATH Path to signing keystore (.jks/.keystore)
+ SIGNING_KEY_ALIAS Alias of signing key
+ SIGNING_KEYSTORE_PASSWORD Keystore password
+ SIGNING_KEY_PASSWORD Key password (defaults to SIGNING_KEYSTORE_PASSWORD)
+
+Optional env vars:
+ ANDROID_HOME Android SDK root (or ANDROID_SDK_ROOT)
+ APK_GLOB Glob for unsigned APKs
+ (default: android/app/build/outputs/apk/release/*-unsigned.apk)
+ ENABLE_SOURCESTAMP true/false (default: false)
+ SOURCESTAMP_KEYSTORE_PATH Path to SourceStamp keystore
+ SOURCESTAMP_KEY_ALIAS Alias in SourceStamp keystore
+ SOURCESTAMP_KEYSTORE_PASSWORD SourceStamp keystore password
+ SOURCESTAMP_KEY_PASSWORD SourceStamp key password
+
+Output:
+ Creates sibling files for each unsigned APK:
+ *-aligned.apk
+ *-signed.apk
+
+Examples:
+ SIGNING_KEYSTORE_PATH=android/keystore/meshchatx-release.jks \
+ SIGNING_KEY_ALIAS=meshchatx-release \
+ SIGNING_KEYSTORE_PASSWORD='...' \
+ SIGNING_KEY_PASSWORD='...' \
+ bash scripts/sign-android-apks.sh
+
+ SIGNING_KEYSTORE_PATH=android/keystore/meshchatx-release.jks \
+ SIGNING_KEY_ALIAS=meshchatx-release \
+ SIGNING_KEYSTORE_PASSWORD='...' \
+ SIGNING_KEY_PASSWORD='...' \
+ ENABLE_SOURCESTAMP=true \
+ SOURCESTAMP_KEYSTORE_PATH=android/keystore/meshchatx-stamp.jks \
+ SOURCESTAMP_KEY_ALIAS=meshchatx-stamp \
+ SOURCESTAMP_KEYSTORE_PASSWORD='...' \
+ SOURCESTAMP_KEY_PASSWORD='...' \
+ bash scripts/sign-android-apks.sh
+EOF
+}
+
+if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
+ usage
+ exit 0
+fi
+
+require_env() {
+ local name="$1"
+ if [[ -z "${!name:-}" ]]; then
+ echo "Missing required env var: ${name}" >&2
+ exit 1
+ fi
+}
+
+require_env SIGNING_KEYSTORE_PATH
+require_env SIGNING_KEY_ALIAS
+require_env SIGNING_KEYSTORE_PASSWORD
+SIGNING_KEY_PASSWORD="${SIGNING_KEY_PASSWORD:-${SIGNING_KEYSTORE_PASSWORD}}"
+
+ANDROID_HOME="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}"
+if [[ -z "${ANDROID_HOME}" ]]; then
+ echo "Set ANDROID_HOME (or ANDROID_SDK_ROOT) to your Android SDK path." >&2
+ exit 1
+fi
+
+BT_DIR="$(ls -d "${ANDROID_HOME}"/build-tools/* 2>/dev/null | sort -V | tail -n 1)"
+if [[ -z "${BT_DIR}" ]]; then
+ echo "No Android build-tools found under ${ANDROID_HOME}/build-tools." >&2
+ exit 1
+fi
+if [[ ! -x "${BT_DIR}/zipalign" || ! -x "${BT_DIR}/apksigner" ]]; then
+ echo "Missing zipalign/apksigner in ${BT_DIR}. Install build-tools via sdkmanager." >&2
+ exit 1
+fi
+
+APK_GLOB="${APK_GLOB:-android/app/build/outputs/apk/release/*-unsigned.apk}"
+shopt -s nullglob
+APKS=( ${APK_GLOB} )
+shopt -u nullglob
+if [[ ${#APKS[@]} -eq 0 ]]; then
+ echo "No unsigned APKs matched: ${APK_GLOB}" >&2
+ exit 1
+fi
+
+ENABLE_SOURCESTAMP="${ENABLE_SOURCESTAMP:-false}"
+if [[ "${ENABLE_SOURCESTAMP}" == "true" ]]; then
+ require_env SOURCESTAMP_KEYSTORE_PATH
+ require_env SOURCESTAMP_KEY_ALIAS
+ require_env SOURCESTAMP_KEYSTORE_PASSWORD
+ SOURCESTAMP_KEY_PASSWORD="${SOURCESTAMP_KEY_PASSWORD:-${SOURCESTAMP_KEYSTORE_PASSWORD}}"
+fi
+
+for apk in "${APKS[@]}"; do
+ base="${apk%-unsigned.apk}"
+ aligned="${base}-aligned.apk"
+ signed="${base}-signed.apk"
+
+ echo "Aligning ${apk}"
+ "${BT_DIR}/zipalign" -p -f 4 "${apk}" "${aligned}"
+
+ sign_args=(
+ sign
+ --ks "${SIGNING_KEYSTORE_PATH}"
+ --ks-key-alias "${SIGNING_KEY_ALIAS}"
+ --ks-pass "pass:${SIGNING_KEYSTORE_PASSWORD}"
+ --key-pass "pass:${SIGNING_KEY_PASSWORD}"
+ --out "${signed}"
+ )
+
+ if [[ "${ENABLE_SOURCESTAMP}" == "true" ]]; then
+ sign_args+=(
+ --stamp-signer
+ --stamp-ks "${SOURCESTAMP_KEYSTORE_PATH}"
+ --stamp-key-alias "${SOURCESTAMP_KEY_ALIAS}"
+ --stamp-ks-pass "pass:${SOURCESTAMP_KEYSTORE_PASSWORD}"
+ --stamp-key-pass "pass:${SOURCESTAMP_KEY_PASSWORD}"
+ )
+ fi
+
+ echo "Signing ${aligned}"
+ "${BT_DIR}/apksigner" "${sign_args[@]}" "${aligned}"
+
+ echo "Verifying ${signed}"
+ "${BT_DIR}/apksigner" verify --verbose --print-certs "${signed}"
+done
+
+echo "Done."


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────